﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Longbow.Logging
{
    /// <summary>
    /// 日志文件内部操作类
    /// </summary>
    internal class FileLoggerWriter : IDisposable
    {
        private BlockingCollection<LogMessage> _messageQueue;
        private CancellationTokenSource _cancellationTokenSource;
        private readonly List<LogMessage> _currentBatch = new List<LogMessage>();
        private string _logFolder;
        private string _logFileName;
        private Task _logTask;

        /// <summary>
        /// 获得/设置 日志配置项 FileLoggerOptions 实例
        /// </summary>
        public FileLoggerOptions Option { get; set; }

        /// <summary>
        /// 默认构造函数
        /// </summary>
        /// <param name="option">日志配置项 FileLoggerOptions 实例</param>
        public FileLoggerWriter(FileLoggerOptions option)
        {
            Option = option;
            _messageQueue = new BlockingCollection<LogMessage>(new ConcurrentQueue<LogMessage>());
            _cancellationTokenSource = new CancellationTokenSource();
            _logTask = Task.Run(ProcessLogQueue);
        }

        /// <summary>
        /// 写消息操作
        /// </summary>
        /// <param name="message">日志消息内容</param>
        public void WriteMessage(string message)
        {
            var msg = new LogMessage { Message = message, Timestamp = DateTimeOffset.Now };
#if NETSTANDARD2_0
            _logFolder = Path.Combine(AppContext.BaseDirectory, Path.GetDirectoryName(Option.FileName));
#else
            _logFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetDirectoryName(Option.FileName));
#endif
            Directory.CreateDirectory(_logFolder);
            RollFilesAsync();

            if (Option.Batch && !_messageQueue.IsAddingCompleted) _messageQueue.Add(msg, _cancellationTokenSource.Token);
            else
            {
                _logFileName = $"{Path.GetFileNameWithoutExtension(Option.FileName)}-{msg.Timestamp:yyyyMMdd}{Path.GetExtension(Option.FileName)}";
                var fullName = Path.Combine(_logFolder, _logFileName);
                WriteToFile(fullName, msg.Message);
            }
        }

        private async Task ProcessLogQueue()
        {
            var cancelSource = CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token, _flushCancellationTokenSource.Token);
            while (!_cancellationTokenSource.IsCancellationRequested)
            {
                var limit = Option.BatchCount;
                while ((force || limit > 0) && _messageQueue.TryTake(out var message))
                {
                    _currentBatch.Add(message);
                    limit--;
                }

                if (_flushCancellationTokenSource.IsCancellationRequested)
                {
                    // force flush output
                    _flushCancellationTokenSource.Dispose();
                    _flushCancellationTokenSource = new CancellationTokenSource();

                    // 恢复强制输出开关
                    force = false;
                    cancelSource.Dispose();
                    cancelSource = CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token, _flushCancellationTokenSource.Token);
                }

                try
                {
                    if (_currentBatch.Any())
                    {
                        WriteMessagesToFile(_currentBatch);
                        _currentBatch.Clear();
                    }
                    await Task.Delay(Math.Max(1000, Option.Interval), cancelSource.Token);
                }
                catch (TaskCanceledException)
                {
                    if (_flushCancellationTokenSource.IsCancellationRequested)
                    {
                        force = true;
                        continue;
                    }
                    break;
                }
            }
            cancelSource.Dispose();

            // flush message to file
            while (_messageQueue.TryTake(out var message)) _currentBatch.Add(message);
            WriteMessagesToFile(_currentBatch);
        }

        private void WriteMessagesToFile(IEnumerable<LogMessage> messages)
        {
            foreach (var group in messages.GroupBy(m => new { m.Timestamp.Year, m.Timestamp.Month, m.Timestamp.Day }))
            {
                _logFileName = $"{Path.GetFileNameWithoutExtension(Option.FileName)}-{group.Key.Year:0000}{group.Key.Month:00}{group.Key.Day:00}{Path.GetExtension(Option.FileName)}";
                var fullName = Path.Combine(_logFolder, _logFileName);
                WriteToFile(fullName, string.Join(string.Empty, group.Select(log => log.Message)));
            }
        }

        private readonly object _writeLocker = new object();
        private void WriteToFile(string fileName, string content)
        {
            lock (_writeLocker)
            {
                using (var streamWriter = File.AppendText(fileName))
                {
                    streamWriter.Write(content);
                }
            }
        }

        private void RollFilesAsync()
        {
            if (Option.MaxFileCount > 0)
            {
                Task.Run(() =>
                {
                    var rollFile = Directory
                    .EnumerateFiles(_logFolder, $"{Path.GetFileNameWithoutExtension(Option.FileName)}*{Path.GetExtension(Option.FileName)}")
                    .FirstOrDefault(f =>
                    {
                        var fn = Path.GetFileNameWithoutExtension(f);
#if NETSTANDARD2_0
                        return fn.Length > 8 && fn.AsSpan().Slice(fn.Length - 8).ToString().CompareTo(DateTimeOffset.Now.AddDays(0 - Option.MaxFileCount).ToString("yyyyMMdd")) < 1;
#else
                        return fn.Length > 8 && fn.Substring(fn.Length - 8).CompareTo(DateTimeOffset.Now.AddDays(0 - Option.MaxFileCount).ToString("yyyyMMdd")) < 1;
#endif
                    });
                    if (!string.IsNullOrEmpty(rollFile)) File.Delete(rollFile);
                });
            }
        }

        private bool force;
        private CancellationTokenSource _flushCancellationTokenSource = new CancellationTokenSource();
        /// <summary>
        /// 强制输出到日志文件中
        /// </summary>
        public void Flush() => _flushCancellationTokenSource.Cancel();

        /// <summary>
        /// Dispose 方法
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private bool disposedValue; // To detect redundant calls

        /// <summary>
        /// Dispose 重载方法
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                disposedValue = true;
                if (disposing)
                {
                    _messageQueue.CompleteAdding();
                    _cancellationTokenSource.Cancel();
                    _logTask.Wait(Option.TaskWaitTime);

                    _cancellationTokenSource.Dispose();
                    _cancellationTokenSource = null;

                    _messageQueue.Dispose();
                    _messageQueue = null;
                }
            }
        }

        private struct LogMessage
        {
            /// <summary>
            /// 时间戳
            /// </summary>
            public DateTimeOffset Timestamp { get; set; }

            /// <summary>
            /// 消息体
            /// </summary>
            public string Message { get; set; }
        }
    }
}
